﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;

namespace System.Cloud.Collections
{
    public static class LinqExtensions
    {
        /// <summary>
        /// Select will project elements of an IReadOnlyObservableList<typeparamref name="T"/> to 
        /// IReadOnlyObservableList<typeparamref name="U"/> while preserving and projecting INotifyCollectionChanged events.  Use this
        /// for projecting a list while ensuring that it works with data binding.
        /// </summary>
        /// <typeparam name="T">Original type of elements in the list.</typeparam>
        /// <typeparam name="U">New type of elements in the projected list</typeparam>
        /// <param name="self">List to project</param>
        /// <param name="projection">Function to project from T to U.</param>
        /// <returns>A projected read-only list that will fire INotifyCollectionChanged events of the appropriate type</returns>
        public static IReadOnlyObservableList<U> Select<T, U>(this IReadOnlyObservableList<T> self, Func<T, U> projection)
        {
            Contract.Requires(self != null);
            Contract.Requires(projection != null);
            Contract.Ensures(Contract.Result<IReadOnlyObservableList<U>>() != null);
            Contract.Ensures(Contract.Result<IReadOnlyObservableList<U>>().Count == self.Count);

            // Implementation-wise, we split a list into two different components - a stream of events
            // for data binding, and the list after projecting the elements to another type.  Note
            // we must also run the projection on each of the events as well, since NotifyCollectionChangedEventArgs
            // contains collections for the new, removed, and modified events.  Afterwards, we glue the new event
            // stream and the projected list together again.
            var obs = Observable.FromEventPattern<NotifyCollectionChangedEventArgs>(self, "CollectionChanged");
            var newEvents = obs.Select(eventPattern => TransformNotifyCollectionChangedEventArgs<T, U>(eventPattern.EventArgs, projection));

            IReadOnlyList<U> projectedList = new ListSelect<T, U>(self, projection);

            // I need a class that implements IReadOnlyList<T> and INotifyCollectionChanged taking an observable stream
            return new ObservableListProxy<U>(projectedList, newEvents);
        }

        internal static NotifyCollectionChangedEventArgs TransformNotifyCollectionChangedEventArgs<TSource, TResult>(NotifyCollectionChangedEventArgs e, Func<TSource, TResult> transform)
        {
            Contract.Requires(e != null);
            Contract.Requires(transform != null);

            IList xformNewItems = null;
            if (e.NewItems != null)
            {
                xformNewItems = new List<TResult>(e.NewItems.Count);
                foreach (TSource oldItem in e.NewItems)
                    xformNewItems.Add(transform(oldItem));
            }

            IList xformOldItems = null;
            if (e.OldItems != null)
            {
                xformOldItems = new List<TResult>(e.OldItems.Count);
                foreach (TSource oldItem in e.OldItems)
                    xformOldItems.Add(transform(oldItem));
            }

            NotifyCollectionChangedEventArgs newEventArgs = null;
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    newEventArgs = new NotifyCollectionChangedEventArgs(e.Action, xformNewItems, e.NewStartingIndex);
                    break;

                case NotifyCollectionChangedAction.Remove:
                    newEventArgs = new NotifyCollectionChangedEventArgs(e.Action, xformOldItems, e.OldStartingIndex);
                    break;

                case NotifyCollectionChangedAction.Replace:
                    // Do we use the old or new starting index here?
                    Contract.Assert(e.NewStartingIndex >= 0);
                    newEventArgs = new NotifyCollectionChangedEventArgs(e.Action, xformNewItems, xformOldItems, e.NewStartingIndex);
                    break;

                case NotifyCollectionChangedAction.Reset:
                    newEventArgs = new NotifyCollectionChangedEventArgs(e.Action);
                    break;

                case NotifyCollectionChangedAction.Move:
                    // Do we use the old or new collections here?
                    Contract.Assert(e.NewItems.Count >= 0);
                    newEventArgs = new NotifyCollectionChangedEventArgs(e.Action, xformNewItems, e.NewStartingIndex, e.OldStartingIndex);
                    break;

                default:
                    Contract.Assume(false, "Unhandled NotifyCollectionChangedAction!");
                    throw new Exception("Unexpected NotifyCollecitonChangedAction!");
            }
            return newEventArgs;
        }
    }

    #region Plumbing Classes

    // Preserves the list-ness of a list when we do a Select on it.  Used to project an IReadOnlyList<T> to an IReadOnlyList<U>.
    internal class ListSelect<T, U> : IReadOnlyList<U>, IList, IDisposable
    {
        Func<T, U> _projection;
        IReadOnlyList<T> _inner;

        internal ListSelect(IReadOnlyList<T> list, Func<T, U> projection)
        {
            _inner = list;
            _projection = projection;
        }

        public void Dispose()
        {
            if (_inner != null)
            {
                IDisposable disp = _inner as IDisposable;
                if (disp != null)
                    disp.Dispose();
                _inner = null;
            }
        }

        public U this[int index]
        {
            get { return _projection(_inner[index]); }
        }

        public int Count
        {
            get { return _inner.Count; }
        }

        public IEnumerator<U> GetEnumerator()
        {
            foreach (T t in _inner)
                yield return _projection(t);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #region IList members

        public int Add(object value)
        {
            throw new NotSupportedException("This list is a read-only projection.");
        }

        public void Clear()
        {
            throw new NotSupportedException("This list is a read-only projection.");
        }

        public bool Contains(object value)
        {
            return IndexOf(value) >= 0;
        }

        public int IndexOf(object value)
        {
            // Lame implementation with bad perf consequences, especially if your object implements IDisposable.
            // Could be better if we had a U -> T projection, though that may not be possible all the time.
            for(int i=0; i<Count; i++)
                if (Object.Equals(this[i], value))
                    return i;
            return -1;
        }

        public void Insert(int index, object value)
        {
            throw new NotSupportedException("This list is a read-only projection.");
        }

        public bool IsFixedSize {
            get { return true; }
        }

        public bool IsReadOnly {
            get { return true; }
        }

        public void Remove(object value)
        {
            throw new NotSupportedException("This list is a read-only projection.");
        }

        public void RemoveAt(int index)
        {
            throw new NotSupportedException("This list is a read-only projection.");
        }

        Object IList.this[int index] {
            get {
                return this[index];
            }
            set {
                throw new NotSupportedException("This list is a read-only projection.");
            }
        }

        public void CopyTo(Array array, int index)
        {
            for (int i = 0; i < _inner.Count; i++)
                array.SetValue(_projection(_inner[i]), index + i);
        }

        public bool IsSynchronized {
            get { return ((ICollection)_inner).IsSynchronized; }
        }

        public Object SyncRoot {
            get { return ((ICollection)_inner).SyncRoot; }
        }

        #endregion IList members
    }

    // Given an IReadOnlyList<T> and a separate stream of NotifyCollectionChanged events, stitch them together
    // into one read-only list that raises the events.  
    internal class ObservableListProxy<T> : IReadOnlyObservableList<T>, INotifyCollectionChanged, IDisposable
    {
        IReadOnlyList<T> _list;
        IObservable<NotifyCollectionChangedEventArgs> _myNotifications;
        IDisposable _subscription;

        internal ObservableListProxy(IReadOnlyList<T> list, IObservable<NotifyCollectionChangedEventArgs> notifier)
        {
            Contract.Requires(list != null);
            Contract.Requires(notifier != null);

            _list = list;
            _myNotifications = notifier;
            _subscription = notifier.Subscribe(nccea =>
            {
                var localHandler = this.CollectionChanged;
                if (localHandler != null)
                    localHandler(this, nccea);
            });
        }

        public void Dispose()
        {
            if (_list != null)
            {
                IDisposable disp = _list as IDisposable;
                if (disp != null)
                    disp.Dispose();
                _list = null;
            }

            if (_subscription != null)
                _subscription.Dispose();
            _subscription = null;
        }

        public event NotifyCollectionChangedEventHandler CollectionChanged;

        public T this[int index] {
            get { return _list[index]; }
        }

        public int Count {
            get { return _list.Count; }
        }

        public IEnumerator<T> GetEnumerator()
        {
            return _list.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _list.GetEnumerator();
        }
    }

    #endregion Plumbing Classes

}
